#!/usr/bin/env python

# Simple GUI that can be associated with .torrent files to easily add
# them to a remote transmission instance.

import sys
import os
from optparse import OptionParser
import pygtk
#pygtk.require('2.4')
import gtk
import re
import base64
try:
    import json
except ImportError:
    import simplejson as json
import httplib
import urllib2
import webbrowser

# TODO: Integrate/automate with distutils
__version__ = '0.1.1'

usage = 'usage: %prog [options] [torrent [torrent ...]]'
version = '%prog ' + __version__
description = '''Opens each torrent file specified, one at a time and
promts the user to select a server and download path for the
torrent. The list of target servers and download paths is configurable
via a JSON formated config file in '/etc/add-remote-torrent.cfg' or
'~/.add-remote-torrent'.'''


class TransmissionRequest:
    """Handle communication with Transmission server. Class stolen from:
    http://github.com/fagga/transmission-remote-cli"""
    def __init__(self, host, port, method=None, tag=None, arguments=None):
        self.hostname = host
        self.url = 'http://%s:%d/transmission/rpc' % (host, port)
        self.open_request  = None
        self.last_update   = 0
        if method:
            self.set_request_data(method, tag, arguments)
 
    def set_request_data(self, method, tag, arguments=None):
        request_data = {'method':method}
        if tag: request_data['tag'] = tag
        if arguments: request_data['arguments'] = arguments
        self.http_request = urllib2.Request(url=self.url, data=json.dumps(request_data))
 
    def send_request(self):
        """Ask for information from server OR submit command."""
 
        try:
            self.open_request = urllib2.urlopen(self.http_request)
        except AttributeError:
            # request data (http_request) isn't specified yet -- data will be available on next call
            pass
        except httplib.BadStatusLine, msg:
            # server sends something httplib doesn't understand.
            # (happens sometimes with high cpu load[?])
            pass  
        except urllib2.HTTPError, msg:
            msg = self.html2text(str(msg.read()))
            m = re.search('X-Transmission-Session-Id:\s*(\w+)', msg)
            try: # extract session id and send request again
                self.http_request.add_header('X-Transmission-Session-Id', m.group(1))
                self.send_request()
            except AttributeError: # a real error occurred
                quit(str(msg) + "\n", CONNECTION_ERROR)
        except urllib2.URLError, msg:
            try:
                reason = msg.reason[1]
            except IndexError:
                reason = str(msg.reason)
            quit("Cannot connect to %s: %s\n" % (self.http_request.host, reason), CONNECTION_ERROR)
 
    def get_response(self):
        """Get response to previously sent request."""
 
        if self.open_request == None:
            return {'result': 'no open request'}
        response = self.open_request.read()
        try:
            data = json.loads(response)
        except ValueError:
            quit("Cannot not parse response: %s\n" % response, JSON_ERROR)
        self.open_request = None
        return data

    def html2text(self, str):
        str = re.sub(r'</h\d+>', "\n", str)
        str = re.sub(r'</p>', ' ', str)
        str = re.sub(r'<[^>]*?>', '', str)
        return str 
# End of Class TransmissionRequest


class AddRemoteTorrent:

    # This is a callback function. The data arguments are ignored
    # in this example. More on callbacks below.
    def add_torrent(self, widget, dest=None):
        args = {'metainfo': base64.b64encode(open(self.torrent).read())}

        # If a download directory was specified, tell transmission to
        # use it
        if dest['directory']:
            args['download-dir'] = dest['directory']

        request = TransmissionRequest(dest['hostname'], 9091, 
                                      method='torrent-add', arguments=args)
        request.send_request()
        response = request.get_response()
        if response['result'] != 'success':
            print "Failed to add torrent: " + response['result']
        elif self.open_web_check_button.get_active():
            # If selected by the user, open up the torrent web
            # interface after adding the torrent
            webbrowser.open('http://' + self.hostname + ':9091')

    def delete_event(self, widget, event, data=None):
        # If you return False in the "delete_event" signal handler,
        # GTK will emit the "destroy" signal.
        return False

    def destroy(self, widget, data=None):
        gtk.main_quit()

    def __init__(self, targets, torrent):
        # Save the name of the torrent (URL or file path)
        self.torrent = torrent
        
        # create a new window
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_title('Add torrent')
        self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
    
        # Connect "delete_event" given by the window manager
        self.window.connect("delete_event", self.delete_event)
    
        # Here we connect the "destroy" event to a signal handler.  
        # This event occurs when we call gtk_widget_destroy() on the window,
        # or if we return FALSE in the "delete_event" callback.
        self.window.connect("destroy", self.destroy)
    
        # Set a more visually pleasing border width of the window.
        self.window.set_border_width(10)

        # Create a vbox to keep things stacked in an orderly way
        self.vbox = gtk.VBox()
        self.vbox.set_spacing(6)

        # Create a frame for each target with a virtical stack of buttons for
        # each of that targets download Destinations
        for target in targets:
            # Create the frame with a buttonbox
            self.frame = gtk.Frame(target['pretty_name'])
            self.vbox.pack_start(self.frame)
            self.buttonbox = gtk.VButtonBox()
            self.buttonbox.set_spacing(6)
            self.buttonbox.set_border_width(6)
            self.frame.add(self.buttonbox)

            # Add a default option: the 'download-dir' option not be specified
            # in the RPC call to 'torrent-add'
            if not 'destinations' in target:
                target['destinations'] = {}
            target['destinations']['Default'] = None

            # Create buttons for all the destinations
            for destname, destpath in target['destinations'].iteritems():
                b = gtk.Button(destname)
                b.set_tooltip_text(destpath)
                b.connect("clicked", self.add_torrent, 
                          {'hostname': target['hostname'], 'directory': destpath})
                b.connect_object("clicked", gtk.Widget.destroy, self.window)
                self.buttonbox.pack_start(b)
                
        self.open_web_check_button = gtk.CheckButton(
            'Open _web interface after adding torrent')
        self.vbox.pack_start(self.open_web_check_button)
        self.window.add(self.vbox)
        self.window.set_resizable(False)
    
        # and the window
        self.window.show_all()

    def main(self):
        # All PyGTK applications must have a gtk.main(). Control ends here
        # and waits for an event to occur (like a key press or mouse event).
        gtk.main()

def choose_torrent():
    chooser = gtk.FileChooserDialog(title='Select a Torrent file to download', 
                                    action=gtk.FILE_CHOOSER_ACTION_OPEN)

    filter = gtk.FileFilter()
    filter.set_name("BitTorrent Files")
    filter.add_mime_type("application/x-bittorrent")
    filter.add_pattern("*.torrent")
    chooser.add_filter(filter)

    filter = gtk.FileFilter()
    filter.set_name("All files")
    filter.add_pattern("*")
    chooser.add_filter(filter)

    response = chooser.run()
    if response == gtk.RESPONSE_OK:
        return chooser.get_response()
    
    return ()

def main():
    config_paths = ['/etc/add-remote-torrent.cfg', 
                    os.path.expanduser('~/.add-remote-torrent')]

    parser = OptionParser(usage=usage, version=version, description=description)
    parser.add_option("-c", "--config", dest="config",
                      help="configuration file to use")

    (options, torrents) = parser.parse_args()

    if options.config:
        config_paths = [options.config]

    # Read in either the system-wide or user config file
    for config in config_paths:
        if os.path.exists(config):
            targets = json.load(open(config))
            break

    # Launch a file chooser dialog if a file name was not provided as
    # an argument
    if len(torrents) < 1:
        torrents = choose_torrent()
    for torrent in torrents:
        new_torrent = AddRemoteTorrent(targets=targets, torrent=torrent)
        new_torrent.main()
    
    
if __name__ == "__main__":
    main()
